#ifdef USE_ESP32_AT

#include <kvdb.h>
#include <plc_rte.h>
#include <lwesp/lwesp_private.h>
#include "lwesp/lwesp_mem.h"
#include "esp32.h"
#include "ArduinoJson.h"

esp32 *EESP;

static lwespr_t lwesp_callback_func(lwesp_evt_t *evt) {
    switch (lwesp_evt_get_type(evt)) {
        case LWESP_EVT_AT_VERSION_NOT_SUPPORTED: {
            lwesp_sw_version_t v_min, v_curr;

            lwesp_get_min_at_fw_version(&v_min);
            lwesp_get_current_at_fw_version(&v_curr);

            logger.info("Current ESP8266 AT version is not supported by library!\r\n");
            logger.info("Minimum required AT version is: %d.%d.%d\r\n", (int) v_min.major, (int) v_min.minor,
                        (int) v_min.patch);
            logger.info("Current AT version is: %d.%d.%d\r\n", (int) v_curr.major, (int) v_curr.minor,
                        (int) v_curr.patch);
            break;
        }
        case LWESP_EVT_INIT_FINISH: {
            logger.info("Library initialized!\r\n");
            break;
        }
        case LWESP_EVT_RESET_DETECTED: {
            logger.info("Device reset detected!\r\n");
            break;
        }
        case LWESP_EVT_WIFI_IP_ACQUIRED: { /* We have IP address and we are fully ready to work */
            if (lwesp_sta_is_joined()) { /* Check if joined on any network */
                lwesp_sys_thread_create(nullptr, "netconn_client", (lwesp_sys_thread_fn) netconn_client_thread, nullptr,
                                        512,
                                        LWESP_SYS_THREAD_PRIO);
            }
            break;
        }
        default:
            break;
    }
    return lwespOK;
}

int esp32::begin(kSerial *_serial) {
    serial = _serial;
    if (lwesp_init(lwesp_callback_func, 1, serial) != lwespOK) {
        logger.error("Cannot initialize LwESP!\r\n");
        return -1;
    } else {
        logger.info("LwESP initialized!\r\n");
        Rtos::create_thread_run("esp", 256, PriorityLow, (void *) &thd_esp32, this);
        return 0;
    }
    EESP = this;
}

[[noreturn]] void esp32::thd_esp32(const void *arg) {
    auto esp = (esp32 *) arg;
    while (true) {
        esp->sleep(0, 10000);
        // if (!lwesp_sta_is_joined())
        // {
        //     /*
        //      * Connect to access point.
        //      *
        //      * Try unlimited time until access point accepts up.
        //      * Check for station_manager.c to define preferred access points ESP should connect to
        //      */
        //     esp->connect_to_preferred_access_point(1);
        // }
        Rtos::Delay(1000);
    }
}

int esp32::ble_start() {
    return 0;
}

void esp32::prv_cmd_event_fn(lwespr_t status, void *arg) {
    /*
     * Command has now successfully finish
     * and callbacks have been properly processed
     */
    auto esp = (esp32 *) arg;
    esp->ap_async_data.command_is_running = 0;

    if (arg == ARG_SCAN) {
        /* Immediately try to connect to access point after successful scan*/
        esp->prv_try_next_access_point();
    }
}

void esp32::prv_try_next_access_point() {
    uint8_t tried = 0;

    /* No action to be done if command is currently in progress or already connected to network */
    if (ap_async_data.command_is_running || lwesp_sta_has_ip()) {
        return;
    }

    /*
     * Process complete list and try to find suitable match
     *
     * Use global variable for indexes to be able to call function multiple times
     * and continue where it finished previously
     */

    /* List all preferred access points */
    for (; ap_async_data.index_preferred_list < LWESP_ARRAYSIZE(ap_list_preferred);
           ap_async_data.index_preferred_list++, ap_async_data.index_scanned_list = 0) {

        /* List all scanned access points */
        for (; ap_async_data.index_scanned_list < ap_list_scanned_len; ap_async_data.index_scanned_list++) {

            /* Find a match if available */
            if (strncmp(ap_list_scanned[ap_async_data.index_scanned_list].ssid,
                        ap_list_preferred[ap_async_data.index_preferred_list].ssid,
                        strlen(ap_list_preferred[ap_async_data.index_preferred_list].ssid)) == 0) {

                /* Try to connect to the network */
                if (!ap_async_data.command_is_running &&
                    lwesp_sta_join(ap_list_preferred[ap_async_data.index_preferred_list].ssid,
                                   ap_list_preferred[ap_async_data.index_preferred_list].pass,
                                   NULL, prv_cmd_event_fn, ARG_CONNECT, 0) == lwespOK) {
                    ap_async_data.command_is_running = 1;

                    /* Go to next index for sub-for loop and exit */
                    ap_async_data.index_scanned_list++;
                    tried = 1;
                    goto stp;
                } else {
                    /* We have a problem, needs to resume action in next run */
                }
            }
        }
    }

    /* Restart scan operation if there was no try to connect and station has no IP */
    if (!tried && !lwesp_sta_has_ip()) {
        prv_scan_ap_command();
    }
    stp:
    return;
}

lwespr_t esp32::prv_evt_fn(lwesp_evt_t *evt) {
    switch (evt->type) {
        case LWESP_EVT_KEEP_ALIVE:
        case LWESP_EVT_WIFI_DISCONNECTED: {
            /* Try to connect to next access point */
            EESP->prv_try_next_access_point();
            break;
        }
        case LWESP_EVT_STA_LIST_AP: {
            /*
             * After scanning gets completed
             * manually reset all indexes for comparison purposes
             */
            EESP->ap_async_data.index_scanned_list = 0;
            EESP->ap_async_data.index_preferred_list = 0;

            /* Actual connection try is done in function callback */
            break;
        }
        default:
            break;
    }
    return lwespOK;
}

lwespr_t esp32::station_manager_connect_to_access_point_async_init() {
    /* Register system event function */
    lwesp_evt_register(prv_evt_fn);

    /*
     * Start scanning process in non-blocking mode
     *
     * This is the only command being executed from non-callback mode,
     * therefore it must be protected against other threads trying to access the same core
     */
    lwesp_core_lock();
    prv_scan_ap_command();
    lwesp_core_unlock();

    /* Return all good, things will progress (from now-on) asynchronously */
    return lwespOK;
}

lwespr_t esp32::connect_to_preferred_access_point(uint8_t unlimited) {
    lwespr_t eres;
    uint8_t tried;

    /*
     * Scan for network access points
     * In case we have access point,
     * try to connect to known AP
     */
    do {
        if (lwesp_sta_has_ip()) {
            return lwespOK;
        }

        /* Scan for access points visible to ESP device */
        logger.debug("Scanning access points...\r\n");
        if ((eres = prv_scan_ap_command_ex(1)) == lwespOK) {
            tried = 0;

            /* Print all access points found by ESP */
            for (size_t i = 0; i < ap_list_scanned_len; i++) {
                logger.debug("AP found: %s, CH: %d, RSSI: %d\r\n", ap_list_scanned[i].ssid, ap_list_scanned[i].ch,
                             ap_list_scanned[i].rssi);
            }

            /* Process array of preferred access points with array of found points */
            for (size_t j = 0; j < LWESP_ARRAYSIZE(ap_list_preferred); j++) {

                /* Go through all scanned list */
                for (size_t i = 0; i < ap_list_scanned_len; i++) {

                    /* Try to find a match between preferred and scanned */
                    if (strncmp(ap_list_scanned[i].ssid, ap_list_preferred[j].ssid, strlen(ap_list_scanned[i].ssid)) ==
                        0) {
                        tried = 1;
                        logger.debug("Connecting to \"%s\" network...\r\n", ap_list_preferred[j].ssid);

                        /* Try to join to access point */
                        if ((eres = lwesp_sta_join(ap_list_preferred[j].ssid, ap_list_preferred[j].pass, NULL, NULL,
                                                   NULL, 1)) == lwespOK) {
                            lwesp_ip_t ip;
                            uint8_t is_dhcp;

                            logger.debug("Connected to %s network!\r\n", ap_list_preferred[j].ssid);

                            lwesp_sta_copy_ip(&ip, NULL, NULL, &is_dhcp);
                            utils_print_ip("Station IP address: ", &ip, "\r\n");
                            logger.debug("; Is DHCP: %d\r\n", (int) is_dhcp);
                            return lwespOK;
                        } else {
                            logger.debug("Connection error: %d\r\n", (int) eres);
                        }
                    }
                }
            }
            if (!tried) {
                logger.debug(
                        "No access points available with preferred SSID!\r\nPlease check station_manager.c file and edit preferred SSID access points!\r\n");
            }
        } else if (eres == lwespERRNODEVICE) {
            logger.debug("Device is not present!\r\n");
            break;
        } else {
            logger.debug("Error on WIFI scan procedure!\r\n");
        }
        if (!unlimited) {
            break;
        }
        Rtos::Delay(100);
    } while (1);
    return lwespERR;
}

void esp32::load() {
    char *buf;
    buf = kvdb.get("wifi_pass");
    if (buf) {
        DynamicJsonDocument doc(1024);
        deserializeJson(doc, buf);
        for (int i = 0; i < doc.size(); i++) {
            String ssid = doc[i]["ssid"];
            String password = doc[i]["pass"];
            ap_list_preferred[i].ssid = ssid.c_str();
            ap_list_preferred[i].pass = password.c_str();
        }
    }
}

int esp32::sleep(int level, uint32_t delay) {
    LWESP_MSG_VAR_DEFINE(msg);

    LWESP_MSG_VAR_ALLOC(msg, 0);
    LWESP_MSG_VAR_SET_EVT(msg, NULL, NULL);
    LWESP_MSG_VAR_REF(msg).cmd_def = LWESP_CMD_GSLP;
    LWESP_MSG_VAR_REF(msg).msg.sleep.delay = delay;

    return lwespi_send_msg_to_producer_mbox(&LWESP_MSG_VAR_REF(msg), lwespi_initiate_cmd, 5000);
    return 0;
}

int esp32_server::begin() {
    Rtos::create_thread_run("ESPServer", 256, PriorityNormal, (void *) &the_server, this);
    return 0;
}

size_t esp32_server::write(uint8_t) {
    return 0;
}

int esp32_server::available() {
    return 0;
}

int esp32_server::read() {
    return 0;
}

int esp32_server::peek() {
    return 0;
}

void esp32_server::loop() {
    lwespr_t res;
    lwesp_netconn_p server, client;
    lwesp_pbuf_p p;

    /* Create netconn for server */
    server = lwesp_netconn_new(LWESP_NETCONN_TYPE_TCP);
    if (server == nullptr) {
        logger.debug("Cannot create server netconn!\r\n");
    }

    /* Bind it to port 23 */
    res = lwesp_netconn_bind(server, 23);
    if (res != lwespOK) {
        logger.debug("Cannot bind server\r\n");
        goto out;
    }

    /* Start listening for incoming connections with maximal 1 client */
    res = lwesp_netconn_listen_with_max_conn(server, 1);
    if (res != lwespOK) {
        goto out;
    }

    /* Unlimited loop */
    while (true) {
        /* Accept new client */
        res = lwesp_netconn_accept(server, &client);
        if (res != lwespOK) {
            break;
        }
        logger.debug("New client accepted!\r\n");
        while (true) {
            /* Receive data */
            res = lwesp_netconn_receive(client, &p);
            if (res == lwespOK) {
                logger.debug("Data received!\r\n");
                lwesp_pbuf_free(p);
            } else {
                logger.debug("Netconn receive returned: %d\r\n", (int) res);
                if (res == lwespCLOSED) {
                    logger.debug("Connection closed by client\r\n");
                    break;
                }
            }
        }
        /* Delete client */
        if (client != nullptr) {
            lwesp_netconn_delete(client);
            client = nullptr;
        }
    }
    /* Delete client */
    if (client != nullptr) {
        lwesp_netconn_delete(client);
        client = nullptr;
    }

    out:
    logger.debug("Terminating netconn thread!\r\n");
    if (server != nullptr) {
        lwesp_netconn_delete(server);
    }
    lwesp_sys_thread_terminate(nullptr);
}

void wifi_reload() {
    if (EESP)
        EESP->load();
}

#endif
